package common.sql;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import java.util.StringJoiner;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import core.config.Factory;
import core.util.ArrayUtil;
import core.util.NumberUtil;
import core.util.bean.Either;

/**
 * パース可変項目保持クラス
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
final class LineParsedItem implements LineParsedNode {

	/** INパターン */
	private static final Pattern PTN_IN = Pattern.compile(".*\\s[iI][nN][\\s]*[\\(]?[\\s]*$");

	/** クエリ */
	private final String item;
	/** 括弧フラグ */
	private boolean parenthesis = false;
	/** 左要素 */
	private final LineParsedItem left;
	/** 右要素 */
	private final LineParsedItem right;

	/**
	 * コンストラクタ
	 * @param val パース対象文字列
	 */
	public LineParsedItem(final String val) {
		ItemParser ip = new ItemParser(val);
		this.left = getParsedItem(ip);
		this.right = null;
		this.item = null;
		this.parenthesis = false;
	}

	/**
	 * コンストラクタ
	 *
	 * @param str クエリ
	 * @param c 括弧フラグ
	 * @param l 左要素
	 * @param r 右要素
	 */
	private LineParsedItem(final String str, final boolean c,
					final LineParsedItem l, final LineParsedItem r) {
		this.left = l;
		this.right = r;
		this.item = str;
		this.parenthesis = c;
	}

	/**
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		StringJoiner sj = new StringJoiner(" ");
		sj.add(Objects.toString(this.left, ""));
		sj.add(Objects.toString(this.item, ""));
		sj.add(Objects.toString(this.right, ""));
		if (this.parenthesis) {
			return "( " + sj.toString().trim() + " )";
		}
		return sj.toString().trim();
	}

	/**
	 * クエリ構築
	 *
	 * @param pmap パラメタマップ
	 * @param plist パラメタリスト
	 * @return クエリ
	 */
	@Override
	public String build(final Map<String, ?> pmap, final List<Object> plist) {

		String lstr = "";
		List<Object> llist = Collections.emptyList();
		if (this.left != null) {
			llist = new ArrayList<>();
			lstr = this.left.build(pmap, llist);
		}

		String mstr = "";
		List<Object> mlist = Collections.emptyList();
		if (this.item != null) {
			mlist = new ArrayList<>();
			mstr = getQuery(pmap, mlist);
		}

		String rstr = "";
		List<Object> rlist = Collections.emptyList();
		if (this.right != null) {
			rlist = new ArrayList<>();
			rstr = this.right.build(pmap, rlist);
		}

		String type = ItemParser.getKeywordType(this.item);
		if (type != null) {
			if ((this.left != null && lstr.isEmpty())
							|| (this.right != null && rstr.isEmpty())) {
				mstr = "";
				mlist = Collections.emptyList();
				if (ItemParser.TYPE_AND.equals(type)) {
					if (llist.isEmpty() && rlist.isEmpty()) {
						lstr = "";
						rstr = "";
					}
				}
			}
		}

		if (lstr.isEmpty() && mstr.isEmpty() && rstr.isEmpty()) {
			return "";
		}

		plist.addAll(llist);
		plist.addAll(mlist);
		plist.addAll(rlist);

		return toString(lstr, mstr, rstr);
	}

	/**
	 * 文字列化
	 * @param lstr 左文字
	 * @param mstr 中文字
	 * @param rstr 右文字
	 * @return 文字列
	 */
	private String toString(final String lstr, final String mstr, final String rstr) {

		StringBuilder sb = new StringBuilder();
		// root
		if (this.item == null) {
			sb.append(" ");
		}
		if (this.parenthesis) {
			sb.append("(");
		}
		sb.append(lstr);
		if (!mstr.isEmpty() && this.left != null) {
			sb.append(" ");
		}
		sb.append(mstr);
		if (!mstr.isEmpty() && this.right != null) {
			sb.append(" ");
		}
		sb.append(rstr);
		if (this.parenthesis) {
			sb.append(")");
		}
		// root
		if (this.item == null) {
			sb.append(" ");
		}
		return sb.toString();
	}

	/**
	 * ターゲット判断
	 *
	 * @param pmap パラメタマップ
	 * @return ターゲットの場合 true を返す。
	 */
	@Override
	public boolean isTarget(final Map<String, ?> pmap) {
		return true;
	}

	/**
	 * クエリパース処理
	 *
	 * @param ip クエリパース
	 * @return パース可変項目
	 */
	private LineParsedItem getParsedItem(final ItemParser ip) {
		boolean space = false;
		StringBuilder sb = new StringBuilder();
		while (true) {
			String str = ip.getNextToken();
			if (str == null) {
				ip.push(sb.toString());
				break;
			}

			String trim = str.trim();
			if (ItemParser.isKeyword(trim)) {
				ip.push(sb.toString());
				ip.push(trim);
				sb = new StringBuilder();
			} else if ("(".equals(str)) {
				ip.push("(");
				LineParsedItem lpi = getParsedItem(ip);
				if (!ItemParser.isKeyword(lpi.item)
								|| ItemParser.getKeywordType(lpi.item) == null) {
					sb.append(lpi.toString());
					space = false;
				} else {
					ip.push(lpi);
				}
			} else if (")".equals(str)) {
				ip.push(sb.toString());
				return popStacked(ip, true);
			} else if ("BETWEEN".equalsIgnoreCase(trim)) {
				throw new IllegalArgumentException(str);
			} else {
				if (!space && 0 < sb.length()) {
					sb.append(" ");
				}
				sb.append(str);
				space = str.endsWith(" ");
			}
		}

		return popStacked(ip, false);
	}

	/**
	 * ポップ処理
	 * @param ip クエリパース
	 * @param p 括弧までフラグ
	 * @return LineParsedItem
	 */
	private LineParsedItem popStacked(final ItemParser ip, final boolean p) {
		String top = null;
		LineParsedItem r = null;
		LineParsedItem l = null;
		while (!ip.isEmpty()) {
			Either<String, LineParsedItem> pop = ip.pop();
			if (pop.isLeft() && "(".equals(pop.left())) {
				if (top != null || (l != null && r != null)) {
					l = new LineParsedItem(top, true, l, r);
					r = null;
					top = null;
				} else if (l != null) {
					l.parenthesis = true;
				}
				if (p) {
					break;
				}
			}

			if (pop.isLeft() && ItemParser.isKeyword(pop.left())) {
				if (top != null) {
					r = new LineParsedItem(top, false, l, r);
					l = null;
				} else {
					if (r == null) {
						r = l;
						l = null;
					}
				}
				top = pop.left();
			} else {
				if (l != null) {
					if (r == null) {
						r = l;
					} else {
						r = new LineParsedItem(top, false, l, r);
						top = null;
					}
				}

				if (pop.isRight()) {
					l = pop.right();
				} else {
					l = new LineParsedItem(pop.left(), false, null, null);
				}
			}
		}

		if (top != null || (l != null && r != null)) {
			return new LineParsedItem(top, false, l, r);
		}

		return l;
	}

	/**
	 * クエリ文字列取得
	 *
	 * @param pmap パラメタマップ
	 * @param plist パラメタリスト
	 * @return クエリ文字列
	 */
	private String getQuery(final Map<String, ?> pmap, final List<Object> plist) {
		StringBuilder sb = null;
		boolean quote = false;
		int prv = 0;
		int loc = 0;
		while (loc < this.item.length()) {
			int ch = this.item.codePointAt(loc);
			if (!quote && LineParsedUtil.isBindCharacter(ch)) {
				String bind = getBindString(this.item, loc);
				String last = this.item.substring(prv, loc);

				String prepare = prepareParameter(pmap, plist, bind, last);
				if (Objects.toString(prepare, "").isEmpty()) {
					return "";
				}

				if (sb == null) {
					sb = new StringBuilder();
				}
				sb.append(last);
				sb.append(prepare);

				loc = loc + bind.length();
				prv = loc;
			} else {
				loc = this.item.offsetByCodePoints(loc, 1);
				if ('\'' == ch) {
					if (loc < this.item.length()) {
						if ('\'' == this.item.codePointAt(loc)) {
							loc = loc + "'".length();
						} else {
							quote = !quote;
						}
					}
				}
			}
		}

		if (sb != null) {
			sb.append(this.item.substring(prv));
			return sb.toString();
		}

		return this.item;
	}

	/**
	 * 項目文字列取得
	 *
	 * @param val 文字列
	 * @param loc 開始位置
	 * @return 項目文字列
	 */
	private String getBindString(final String val, final int loc) {
		int e = loc;
		do {
			e = LineParsedUtil.separatorNext(val, e);
		} while (e < val.length() && LineParsedUtil.isBindCharacter(val.codePointAt(e)));

		while (e < val.length() && Character.isWhitespace(val.codePointAt(e))) {
			e = val.offsetByCodePoints(e, 1);
		}

		if (e < val.length() && val.codePointAt(e) == '(') {
			e = endEnclosure(val, e) + 1;
		} else if (e < val.length() && val.codePointAt(e) == '\'') {
			e = val.indexOf('\'', e + "'".length()) + "'".length();
		} else if (LineParsedUtil.isDate(val.substring(e))
						|| LineParsedUtil.isTimestamp(val.substring(e))) {
			e = val.indexOf(')', e) + ")".length();
		} else if (LineParsedUtil.isRplace(val.codePointAt(loc))) {
			e = val.length();
		} else {
			while (e < val.length()) {
				int cp = val.codePointAt(e);
				if (!(Character.isDigit(cp) && cp < Byte.MAX_VALUE) && cp != '-' && cp != '.') {
					break;
				}
				e = val.offsetByCodePoints(e, 1);
			}
		}

		return val.substring(loc, e);
	}

	/**
	 * 終了括弧位置取得
	 * @param val 文字列
	 * @param begin 括弧開始位置
	 * @return 終了括弧位置
	 */
	private int endEnclosure(final String val, final int begin) {
		int parenthsis = 0;
		for (int i = begin; i < val.length(); i = val.offsetByCodePoints(i, 1)) {
			int cp = val.codePointAt(i);
			if (cp == '(') {
				parenthsis++;
			} else if (cp == ')') {
				parenthsis--;
				if (parenthsis == 0) {
					return i;
				}
			}
		}
		return val.length();
	}

	/**
	 * パラメタリスト準備とパラメタ文字列返却
	 *
	 * @param pmap パラメタマップ
	 * @param plist パラメタリスト
	 * @param bind bind値
	 * @param last bind直前文字列
	 * @return パラメタ文字列
	 */
	private String prepareParameter(final Map<String, ?> pmap, final List<Object> plist,
					final String bind, final String last) {
		if (LineParsedUtil.isMultiKey(bind)) {
			return prepareMulti(pmap, plist, bind);
		}
		return prepare(pmap, plist, bind, PTN_IN.matcher(last).matches());
	}

	/**
	 * パラメタリスト準備とパラメタ文字列返却
	 *
	 * @param pmap パラメタマップ
	 * @param plist パラメタリスト
	 * @param bind bind値
	 * @return パラメタ文字列
	 */
	private String prepareMulti(final Map<String, ?> pmap,
					final List<Object> plist, final String bind) {
		String ret = null;
		List<String> keys = LineParsedUtil.getMultiKeys(bind);
		if (!keys.isEmpty()) {
			List<List<Object>> vl = getValueList(keys, pmap);
			if (vl != null) {
				ret = prepareMultiString(vl, plist);
			} else {
				String val = LineParsedUtil.getLiteral(bind);
				if (LineParsedUtil.isSearchless(bind)) {
					plist.addAll(Arrays.asList(new String[keys.size()]));
					ret = String.join(",", ArrayUtil.fill("?", new String[keys.size()]));
				} else if (LineParsedUtil.isDefault(bind)) {
					List<Object> list = getDefault(val);
					plist.addAll(list);
					ret = String.join(",", ArrayUtil.fill("?", new String[list.size()]));
				}

				if (ret != null) {
					ret = "( " + ret + " )";
				}
			}
		}
		return ret;
	}

	/**
	 * 値リスト取得
	 * @param keys キーリスト
	 * @param pmap パラメタマップ
	 * @return 値リスト
	 */
	private List<List<Object>> getValueList(final List<String> keys, final Map<String, ?> pmap) {
		List<List<Object>> ret = new ArrayList<>();
		for (final String key : keys) {
			Object obj = pmap.get(key);
			if (!LineParsedUtil.isValid(obj)) {
				return null;
			}
			ret.add(toParamCollection(obj, true));
		}
		return ret;
	}

	/**
	 * パラメタ文字列返却
	 *
	 * @param vl パラメタ値
	 * @param plist パラメタリスト
	 * @return パラメタ文字列
	 */
	private String prepareMultiString(final List<List<Object>> vl, final List<Object> plist) {
		StringJoiner ret = new StringJoiner(",");

		List<Object> list = new ArrayList<>();
		for (int i = 0; i < vl.get(0).size(); i++) {
			StringJoiner sj = new StringJoiner(",");
			for (int j = 0; j < vl.size(); j++) {
				Object obj = vl.get(j).get(i);
				if (!Objects.toString(obj, "").isEmpty() || 1 == vl.get(j).size()) {
					list.add(obj);
					sj.add("?");
				}
			}
			ret.add("( " + sj.toString() + " )");
		}
		plist.addAll(list);

		return ret.toString();
	}

	/**
	 * パラメタリスト準備とパラメタ文字列返却
	 *
	 * @param pmap パラメタマップ
	 * @param plist パラメタリスト
	 * @param bind bind値
	 * @param in IN含フラグ
	 * @return パラメタ文字列
	 */
	private String prepare(final Map<String, ?> pmap, final List<Object> plist,
					final String bind, final boolean in) {
		String key = LineParsedUtil.getKey(bind);
		String val = LineParsedUtil.getLiteral(bind);

		if (LineParsedUtil.isRplace(bind)) {
			int loc = key.indexOf('/');
			if (loc < 0) {
				Object obj = pmap.get(key);
				if (LineParsedUtil.isValid(obj)) {
					return toCsvString(obj);
				}
			} else {
				Object obj = pmap.get(key.substring(0, loc));
				if (LineParsedUtil.isValid(obj)) {
					String rep = key.substring(key.offsetByCodePoints(loc, 1));
					String csv = toCsvString(obj);
					return val.replace(rep, csv);
				}
			}
			return val;
		}

		String ret = null;
		Object obj = pmap.get(key);
		if (LineParsedUtil.isValid(obj)) {
			ret = prepareString(val, toParamCollection(obj, in), plist);
		} else {
			if (LineParsedUtil.isSearchless(bind)) {
				plist.add(null);
				ret = "?";
			} else if (LineParsedUtil.isDefault(bind)) {
				List<Object> list = getDefault(val);
				plist.addAll(list);
				ret = String.join(",", ArrayUtil.fill("?", new String[list.size()]));
			}
		}

		if (ret != null && LineParsedUtil.isEnclosed(val)) {
			ret = "( " + ret + " )";
		}
		return ret;
	}

	/**
	 * 文字列化
	 * @param obj パラメタ値
	 * @return コレクションの文字列
	 */
	private String toCsvString(final Object obj) {
		return toList(obj).stream().filter(Objects::nonNull).
							map(Objects::toString).
							map(s -> s.replaceAll("[^0-9A-Za-z_, \\.\\*]", "")).
							collect(Collectors.joining(", "));
	}

	/**
	 * パラメタ文字列返却
	 *
	 * @param val リテラル値
	 * @param l パラメタ値リスト
	 * @param plist パラメタリスト
	 * @return パラメタ文字列
	 */
	private String prepareString(final String val, final List<?> l, final List<Object> plist) {
		StringJoiner ret = new StringJoiner(",");

		List<Object> list = new ArrayList<>();
		for (final Object o : l) {
			Object object = toValidClass(o, val);
			if (!Objects.toString(object, "").isEmpty() || 1 == l.size()) {
				list.add(object);
				ret.add("?");
			}
		}
		plist.addAll(list);

		return ret.toString();
	}

	/**
	 * SQLファイル記述値取得
	 * @param val リテラル値
	 * @return SQLファイル記述値
	 */
	private List<Object> getDefault(final String val) {
		List<Object> ret = new ArrayList<>();

		String str = LineParsedUtil.getInnerValue(val);
		if (LineParsedUtil.isString(str)) {
			for (final String s : str.split(",")) {
				String v = s.trim();
				ret.add(v.substring("'".length(), v.length() - "'".length()));
			}
		} else if (LineParsedUtil.isDate(str)) {
			for (final String line : str.split("[tT][oO]_[dD][aA][tT][eE]")) {
				String date = LineParsedUtil.getDateValue(line);
				if (!Objects.toString(date, "").isEmpty()) {
					ret.add(LineParsedUtil.toDate(date));
				}
			}
		} else if (LineParsedUtil.isTimestamp(str)) {
			for (final String line : str.split("[tT][oO]_[tT][iI][mM][eE][sS][tT][aA][mM][pP]")) {
				String date = LineParsedUtil.getDateValue(line);
				if (!Objects.toString(date, "").isEmpty()) {
					ret.add(LineParsedUtil.toTimestamp(date, line));
				}
			}
		} else {
			for (final String s : str.split(",")) {
				ret.add(toNumber(s));
			}
		}

		return ret;
	}

	/**
	 * SQLとデータを考慮して集合化
	 * @param obj 値オブジェクト
	 * @return 集合
	 */
	private List<Object> toList(final Object obj) {
		if (List.class.isInstance(obj)) {
			return List.class.cast(obj);
		}
		if (obj.getClass().isArray()) {
			return Arrays.asList(Object[].class.cast(obj));
		}
		return Collections.singletonList(obj);
	}

	/**
	 * SQLとデータを考慮して集合化
	 * @param obj 値オブジェクト
	 * @param in IN含フラグ
	 * @return 集合
	 */
	private List<Object> toParamCollection(final Object obj, final boolean in) {
		List<Object> list = toList(obj);
		if (in) {
			if (list.size() == 1) {
				if (list.get(0) != null) {
					list = Factory.cast(Arrays.asList(String.valueOf(list.get(0)).split(",")));
				} else {
					list = Collections.emptyList();
				}
			}
		} else {
			if (!list.isEmpty()) {
				list = Collections.singletonList(list.get(0));
			}
		}
		return list;
	}

	/**
	 * 型変換
	 * @param obj 値
	 * @param val リテラル値
	 * @return 変換後値
	 */
	private Object toValidClass(final Object obj, final String val) {
		String inner = LineParsedUtil.getInnerValue(val);
		if (LineParsedUtil.isString(inner)) {
			return addLikePlace(inner, obj);
		}
		if (LineParsedUtil.isDate(inner)) {
			return LineParsedUtil.toDate(obj);
		}
		if (LineParsedUtil.isTimestamp(inner)) {
			return LineParsedUtil.toTimestamp(obj, inner);
		}
		return toNumber(obj);
	}

	/**
	 * LIKEワイルドカード追加
	 * @param val リテラル値
	 * @param str 追加対象文字列
	 * @return 追加後文字列
	 */
	private String addLikePlace(final String val, final Object str) {
		String ret = Objects.toString(str, "");
		if (val.startsWith("'%")) {
			ret = "%" + ret;
		}
		if (val.endsWith("%'")) {
			ret = ret + "%";
		}
		return ret;
	}

	/**
	 * 数値化
	 * @param obj オブジェクト
	 * @return 数値
	 */
	private Number toNumber(final Object obj) {
		if (Number.class.isInstance(obj)) {
			return Number.class.cast(obj);
		}
		return NumberUtil.toBigDecimal(Objects.toString(obj, null));
	}


	/**
	 * クエリパースクラス
	 *
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	private static final class ItemParser {
		/** キーワードタイプ（両方） */
		public static final String TYPE_AND = "AND";
		/** キーワードタイプ（片方） */
		public static final String TYPE_OR = "OR";

		/** キーワードマップ */
		private static final Map<String, String> KEYWORDS;

		/** 括弧スタック */
		private final Stack<Either<String, LineParsedItem>> stack = new Stack<>();
		/** クエリ文字列 */
		private final String qry;
		/** パース位置 */
		private int loc;

		static {
			Map<String, String> map = new HashMap<>();
			map.put("AND", TYPE_AND);
			map.put("OR", TYPE_OR);
			map.put("WHERE", null);
			KEYWORDS = Collections.unmodifiableMap(map);
		}

		/**
		 * コンストラクタ
		 *
		 * @param query クエリ文字列
		 */
		public ItemParser(final String query) {
			this.qry = query;
			this.stack.clear();
			this.loc = 0;
		}

		/**
		 * スタックプッシュ
		 * @param obj 値
		 */
		public void push(final LineParsedItem obj) {
			if (obj != null) {
				this.stack.push(Either.right(obj));
			}
		}

		/**
		 * スタックプッシュ
		 * @param val 値
		 */
		public void push(final String val) {
			String str = Objects.toString(val, "").trim();
			if (!str.isEmpty()) {
				this.stack.push(Either.left(str));
			}
		}

		/**
		 * スタックポップ
		 * @return 値
		 */
		public Either<String, LineParsedItem> pop() {
			return this.stack.pop();
		}

		/**
		 * スタック空確認
		 * @return からの場合 true を返す。
		 */
		public boolean isEmpty() {
			return this.stack.isEmpty();
		}

		/**
		 * キーワード判断
		 *
		 * @param val 判断対象文字列
		 * @return キーワードの場合 true を返す。
		 */
		public static boolean isKeyword(final String val) {
			return val != null && KEYWORDS.containsKey(val.toUpperCase(Locale.ENGLISH));
		}

		/**
		 * キーワード判断
		 *
		 * @param val 判断対象文字列
		 * @return キーワードの場合 true を返す。
		 */
		public static String getKeywordType(final String val) {
			return val != null ? KEYWORDS.get(val.toUpperCase(Locale.ENGLISH)) : null;
		}

		/**
		 * 次トークン取得
		 *
		 * @return 次トークン
		 */
		public  String getNextToken() {
			int s = this.loc;
			while (s < this.qry.length() && Character.isWhitespace(this.qry.codePointAt(s))) {
				s = this.qry.offsetByCodePoints(s, 1);
			}

			boolean literal = false;
			int e = s;
			while (e < this.qry.length()) {
				int cp = this.qry.codePointAt(e);
				if (cp == '\'') {
					literal = !literal;
				} else if (Character.isWhitespace(cp)) {
					if (!literal) {
						if (s == e) {
							s = this.qry.offsetByCodePoints(e, 1);
						} else {
							e = this.qry.offsetByCodePoints(e, 1);
							break;
						}
					}
				} else if (cp == '(' || cp == ')') {
					if (s == e) {
						e = this.qry.offsetByCodePoints(e, 1);
					}
					break;
				}
				e = this.qry.offsetByCodePoints(e, 1);
			}

			if (s < e) {
				this.loc = e;
				return this.qry.substring(s, e);
			}

			return null;
		}
	}
}
